// 
// Copyright (c) 2006-2008 Ben Motmans
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// Author(s):
//    Ben Motmans <ben.motmans@gmail.com>
//

using System;
using System.IO;
using System.Text;
using System.Reflection;
using System.Globalization;
using System.Collections.Generic;

namespace Anculus.Core.Extended
{
	//TODO: investigate the following sits
	//http://www.gnu.org/software/gettext/manual/html_node/C_0023.html
	//http://www.mono-project.com/I18nGettext
	public sealed class ManagedGettextBackend : AbstractTranslationBackend
	{
		private Dictionary<string, TranslationContainer> _translations;
		
		public ManagedGettextBackend (string catalog, string directory)
		{
			if (String.IsNullOrEmpty (catalog))
				throw new ArgumentException ("catalog");
			if (directory == null || !Directory.Exists (directory))
				throw new ArgumentException ("directory");
			directory = Path.GetFullPath (directory);
			
			_translations = new Dictionary<string,TranslationContainer> ();
			
			Stream stream = GetTranslationCatalogFileStream (catalog, directory);
			if (stream == null)
				Log.Warn ("Unable to locate translation catalog. (culture={0})", _culture.Name);
			else
				LoadFromStream (stream);
		}
		
		public ManagedGettextBackend (string catalog, Assembly assembly)
		{
			if (String.IsNullOrEmpty (catalog))
				throw new ArgumentException ("catalog");
			if (assembly == null)
				throw new ArgumentException ("assembly");
			
			_translations = new Dictionary<string,TranslationContainer> ();
			
			Stream stream = GetTranslationCatalogResourceStream (catalog, assembly);
			if (stream == null)
				Log.Warn ("Unable to locate translation catalog. (culture={0})", _culture.Name);
			else
				LoadFromStream (stream);
		}
		
		protected override string LookupSingular (string key)
		{
			TranslationContainer tc;
			if (_translations.TryGetValue (key, out tc))
				return tc.Singular;
			return key;
		}

		protected override string LookupPlural (string key, string plural, int n)
		{
			TranslationContainer tc;
			if (_translations.TryGetValue (key, out tc)) {
				string p = tc.GetPlural (n);
				if (p != null)
					return p;
			}
			return plural;
		}

		//http://www.gnu.org/software/gettext/manual/html_mono/gettext.html#MO-Files
		//http://live.gnome.org/TranslationProject/DevGuidelines#plurals
		private void LoadFromStream (Stream stream)
		{
			if (!stream.CanRead) {
				Log.Error ("Unable to read from stream");
				return;
			}
			
			byte[] header = new byte[28];
			int read = stream.Read (header, 0, 28);
			if (read != 28) {
				Log.Error ("Invalid .mo file format (incorrect header).");
				return;
			}

			uint magic = BitConverter.ToUInt32 (header, 0);
			if (magic != 0x950412de/*same endianess*/ && magic != 0xde120495 /*different endianess*/) {
				Log.Error ("Invalid .mo file format (invalid magic number).");
				return;
			}
			bool swap = magic == 0xde120495;
			//TODO: swap byte order if inverted
			
			int revision = ReadInt (header, 4, swap);
			if (revision != 0) {
				Log.Error ("Invalid .mo file format (invalid revision number).");
				return;
			}
			
			int n = ReadInt (header, 8, swap);
			int keyOffset = ReadInt (header, 12, swap);
			int translationOffset = ReadInt (header, 16, swap);
			int tableSize = ReadInt (header, 20, swap);
			int tableOffset = ReadInt (header, 24, swap);
			int lookupOffset = tableOffset + (tableSize * 4);
			
			if (stream.Length < lookupOffset) {
				Log.Error ("Invalid .mo file format (invalid offset).");
				return;
			}
			
			int[] keyLengths = new int[n];
			int[] keyOffsets = new int[n];
			int[] translationLengths = new int[n];
			int[] translationOffsets = new int[n];
			
			byte[] segment = new byte[8];
			int i;
			
			//TODO: handle plurals correctly
			
			//read key positions
			stream.Seek (keyOffset, SeekOrigin.Begin);
			for (i = 0; i < n; i++) {
				stream.Read (segment, 0, 8);
				keyLengths[i] = ReadInt (segment, 0, swap);
				keyOffsets[i] = ReadInt (segment, 4, swap);
				
				keyOffset += 8;
			}
			
			//read translation string positions
			stream.Seek (translationOffset, SeekOrigin.Begin);
			for (i = 0; i < n; i++) {
				stream.Read (segment, 0, 8);
				translationLengths[i] = ReadInt (segment, 0, swap);
				translationOffsets[i] = ReadInt (segment, 4, swap);
				
				translationOffset += 8;
			}
			
			//skip hashtable
			stream.Seek (lookupOffset, SeekOrigin.Begin);
			
			//TODO: add code to skip the first entry if it's empty (=skip the info header)

			//read actual keys
			string[] keys = new string[n];
			byte[] tmp = null;
			for (i = 0; i < n; i++) {
				tmp = new byte[keyLengths[i]];
				stream.Seek (keyOffsets[i], SeekOrigin.Begin);
				stream.Read (tmp, 0, keyLengths[i]);
				
				string singularKey, pluralKey;
				ReadKeyStrings (tmp, out singularKey, out pluralKey);
				keys[i] = singularKey;
				
				//FIXME: what do we do with the pluralKey ?
			}
			
			//read actual translations and add to hashtable
			for (i = 0; i < n; i++) {
				tmp = new byte[translationLengths[i]];
				stream.Seek (translationOffsets[i], SeekOrigin.Begin);
				stream.Read (tmp, 0, translationLengths[i]);

				_translations.Add (keys[i], ReadTranslationStrings (tmp));
			}
		}
		
		//http://www.gnu.org/software/gettext/manual/gettext.html#The-LANGUAGE-variable
		//http://www.gnu.org/software/gettext/manual/gettext.html#Locale-Environment-Variables
		private static string[] GetLanguageIdentifiers ()
		{
			List<string> languages = new List<string> ();
			
			//1. LANGUAGE
			string[] env = GetParsedEnvironmentVariable ("LANGUAGE");
			if (env != null)
				languages.AddRange (env);
			
			//2. LC_ALL
			env = GetParsedEnvironmentVariable ("LC_ALL");
			if (env != null)
				languages.AddRange (env);
			
			//3. LC_MESSAGES
			env = GetParsedEnvironmentVariable ("LC_MESSAGES");
			if (env != null)
				languages.AddRange (env);
			
			//4. LANG (automatically parsed by mono, we can just use the current culture
			languages.Add (_culture.Name.Replace ('-','_')); //eg: nl_BE
			languages.Add (_culture.TwoLetterISOLanguageName); //ISO 639-1 fallback, eg: nl
			
			return languages.ToArray ();
		}
		
		private static string[] GetParsedEnvironmentVariable (string variable)
		{
			string value = Environment.GetEnvironmentVariable (variable);
			if (String.IsNullOrEmpty (value))
				return null;
			
			string[] chunks = value.Split (':'); //eg: nb:no
			List<string> languages = new List<string> ();
			
			foreach (string chunk in chunks) {
				//eg: en_US.UTF-8
				int dotIndex = chunk.IndexOf (".");
				if (dotIndex < 0)
					dotIndex = chunk.Length;
				
				int underscoreIndex = chunk.IndexOf ("_");
				
				string main = chunk.Substring (0, underscoreIndex).ToLower (); // 'en'
				string dialect = chunk.Substring (underscoreIndex + 1, dotIndex - underscoreIndex - 1).ToUpper (); // 'US'
				
				languages.Add (String.Concat (main, "_", dialect));
				languages.Add (main);
			}
			
			return languages.ToArray ();
		}
		
		private Stream GetTranslationCatalogFileStream (string catalog, string directory)
		{
			foreach (string name in GetLanguageIdentifiers ()) {
				string dir = Path.Combine (Path.Combine (directory, name), "LC_MESSAGES");

				string[] files = new string[] {
					Path.Combine (dir, catalog + ".mo"),
					Path.Combine (dir, catalog + ".gmo")
				};
				
				if (File.Exists (files[0]))
					return new FileStream (files[0], FileMode.Open, FileAccess.Read);
				else if (File.Exists (files[1]))
					return new FileStream (files[1], FileMode.Open, FileAccess.Read);
			}
			return null;
		}
		
		private Stream GetTranslationCatalogResourceStream (string catalog, Assembly assembly)
		{	
			string[] cultureNames = GetLanguageIdentifiers ();
			
			//use the list of all resources, because guessing with try-catch is too costly
			string[] resources = assembly.GetManifestResourceNames ();

			foreach (string resource in resources) {
				foreach (string name in cultureNames) {
					if (resource == String.Concat (catalog, ".", name, ".mo")
						|| resource == String.Concat (catalog, ".", name, ".gmo"))
						return assembly.GetManifestResourceStream (resource);
				}
			}
			return null;
		}
		
		private static int ReadInt (byte[] segment, int pos, bool swap)
		{
			if (swap) {
				//different Endianess, swap bytes first
				byte[] tmp = new byte[4];
				tmp[0] = segment[pos + 3];
				tmp[1] = segment[pos + 2];
				tmp[2] = segment[pos + 1];
				tmp[3] = segment[pos];
				
				return BitConverter.ToInt32 (tmp, 0);
			} else {
				//the machine is using the same Endianess as the .mo file
				return BitConverter.ToInt32 (segment, pos);
			}
		}
		
		private static void ReadKeyStrings (byte[] segment, out string singularKey, out string pluralKey)
		{
			int index = -1;
			int len = segment.Length;
			for (int i=0; i<len; i++) {
				if (segment[i] == 0) {
					index = i;
					break;
				}
			}
			
			if (index < 0) {
				singularKey = Encoding.UTF8.GetString (segment);
				pluralKey = Encoding.UTF8.GetString (segment);
			} else {
				singularKey = Encoding.UTF8.GetString (segment, 0, index);
				pluralKey = Encoding.UTF8.GetString (segment, index + 1, len - index - 2);
			}
		}
		
		//translation strings can contains several NUL terminated substrings for each plural form
		private static TranslationContainer ReadTranslationStrings (byte[] segment)
		{
			List<string> strings = new List<string> ();
			
			int len = segment.Length;
			int prev = 0;

			for (int i=0; i<len; i++) {
				if (segment[i] == 0) {
					strings.Add (Encoding.UTF8.GetString (segment, prev, i - prev));
					
					prev = i + 1;
				}
			}
			strings.Add (Encoding.UTF8.GetString (segment, prev, len - prev));
			
			return new TranslationContainer (strings.ToArray ());
		}
		
		internal struct TranslationContainer
		{
			private string[] _strings;
			
			internal TranslationContainer (string[] strings)
			{
				_strings = strings;
			}
			
			internal string Singular 
			{
				get { return _strings[0]; }
			}
			
			internal string GetPlural (int n)
			{
				if (n < 0)
					return null;
				else if (n >= _strings.Length) //TODO: verify that this is the correct behaviour
					return _strings[_strings.Length - 1];
				else
					return _strings[n];
			}
		}
	}
}
